package com.albertattard.jff;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.swing.SwingWorker;

/**
 * This worker starts by searching for Jar files in a list of directories. The
 * search is done in a recursive manner. Then, once all Jar files under the
 * given directories are listed, the search for the class commence. Finally, a
 * list of Jar files that contain the class being searched is returned.
 * <p>
 * This Worker can be improved by caching the Jar files found under a particular
 * directory. This can be done by using a caching mechanism to retrieve the Jar
 * Files. The problem with that is that new Jar files, added after the last
 * cached, will not be search.
 * 
 * @author Albert Attard
 */
public class JarFinderWorker extends SwingWorker<List<String>, String> {

	/**
	 * This is a common directory filter used to perform a recursive search. It
	 * only accepts non-hidden directories.
	 */
	private static final FileFilter DIR_FILE_FILTER = new FileFilter() {
		/**
		 * Returns true if the file is not hidden and is a directory
		 * 
		 * @param file
		 *            the file being processed
		 * @return true if the file is not hidden and is a directory
		 */
		public boolean accept(final File file) {
			return file.isDirectory() && !file.isHidden();
		}
	};

	/**
	 * This is a Jar file filter. It only accepts <code>.jar</code> or
	 * <code>.zip</code> files. Returns true if the file is not hidden and is a
	 * directory
	 */
	private static final FileFilter JAR_FILENAME_FILTER = new FileFilter() {
		/**
		 * Returns true if the file is not hidden, is a file (not directory) and
		 * the file has the <code>.jar</code> or <code>.zip</code> extension.
		 * 
		 * @param file
		 *            the file being processed
		 * @return true if the file is not hidden, is a file (not directory) and
		 *         the file has the <code>.jar</code> or <code>.zip</code>
		 *         extension.
		 */
		public boolean accept(final File file) {
			final String name = file.getName().toLowerCase();
			return file.isFile() && !file.isHidden()
					&& (name.endsWith(".jar") || name.endsWith(".zip"));
		}
	};

	/** The class full name, that is the package followed by the class name */
	private final String classFullName;

	/**
	 * The array of directories from where the search will begin. All Jar files
	 * under these directories (directly or indirectly).
	 */
	private final String[] directoriesList;

	/**
	 * A list of Jar/Zip files that were not properly searched due to an
	 * error/exception
	 */
	private final List<File> failedFiles = new ArrayList<File>();

	/** The number of Jar files found */
	private int jarFilesFound;

	/**
	 * Creates an instance of this worker.
	 * <p>
	 * Example,
	 * <code>new JarFinderWorker(new String[]{"c:","g:"}, "java.lang.String").execute()</code>
	 * will search for the class <code>java.lang.String</code> in drives
	 * <code>c</code> and <code>g</code>.
	 * 
	 * @param directoriesList
	 *            the top directories from where the search will begin
	 * @param classFullName
	 *            The class full name, that is the package followed by the class
	 *            name
	 */
	public JarFinderWorker(final String[] directoriesList,
			final String classFullName) {
		// The class name is changed from "java.lang.String" to
		// "java/lang/String" as it is represented in the JarEntry.
		this.classFullName = classFullName.replaceAll("\\.", "/").toLowerCase();

		// Creates a defensive copy so that the any changes to the given array
		// will not affect our array.
		this.directoriesList = Arrays.copyOf(directoriesList,
				directoriesList.length);
	}

	/**
	 * Starts by searching for all Jar files under the given directories and
	 * then search for the class within these Jar files. It then returns a list
	 * of Jar files' absolute paths that contain the class being search.
	 * 
	 * @return the list of Jar files that contain the class
	 */
	@Override
	protected List<String> doInBackground() throws Exception {
		// Get all Jar files under the given search directories
		final List<File> jarFilesList = getJarsList();
		jarFilesFound = jarFilesList.size();

		// The list that will contain the Jar files that contain the class
		final List<String> list = new ArrayList<String>();

		// For every Jar file found, search for the class and update the
		// progress.
		for (int i = 0, size = jarFilesList.size(); i < size; i++) {
			if (isClassInJar(classFullName, jarFilesList.get(i))) {
				// Get the jar file absolute path, publish it and added to the
				// list.
				final String file = jarFilesList.get(i).getAbsolutePath();
				publish(file);
				list.add(file);
			}
			// Update the progress
			setProgress(((i + 1) * 100) / size);
		}

		return list;
	}

	/**
	 * 
	 * @return
	 */
	public Iterator<File> getFailedJars(){
		return failedFiles.iterator();
	}

	/**
	 * Returns the number of Jar files found under the given directories.
	 * 
	 * @return the number of Jar files found under the given directories
	 */
	public int getJarFilesFound() {
		return jarFilesFound;
	}

	/**
	 * Returns the list of Jar files under the given root/search directories (
	 * {@link #directoriesList}).
	 * 
	 * @return the list of Jar files under the given root/search directories
	 */
	private List<File> getJarsList() {
		final List<File> jarFilesList = new ArrayList<File>();
		for (final String dir : directoriesList) {
			final File file = new File(dir);
			if (file.isDirectory()) {
				jarFilesList.addAll(getJarsList(file));
			}
		}

		return jarFilesList;
	}

	/**
	 * Returns the list of Jar files found in this directory and in sub
	 * directory using recursion.
	 * 
	 * @param directory
	 *            the directory from where to search
	 * @return the list of Jar files found in this directory and in sub
	 *         directory using recursion
	 */
	private List<File> getJarsList(final File directory) {
		final File[] jarFilesFound = directory
				.listFiles(JarFinderWorker.JAR_FILENAME_FILTER);

		final List<File> list = new ArrayList<File>();

		if (jarFilesFound != null) {
			for (final File jarFile : jarFilesFound) {
				list.add(jarFile);
			}
		}

		final File[] dirs = directory
				.listFiles(JarFinderWorker.DIR_FILE_FILTER);
		if (dirs != null) {
			for (final File dir : dirs) {
				list.addAll(getJarsList(dir));
			}
		}

		return list;
	}
	
	/**
	 * 
	 * @param classFullName
	 * @param file
	 * @return
	 * @throws IOException
	 */
	private boolean isClassInJar(final String classFullName, final File file) {

		try {
			final JarFile jarFile = new JarFile(file);

			for (final Enumeration<JarEntry> e = jarFile.entries(); e
					.hasMoreElements();) {
				final JarEntry jarEntry = e.nextElement();
				if (jarEntry.getName().toLowerCase().contains(classFullName)) {
					return true;
				}
			}
		} catch (final IOException e) {
			// Ideally we say why this failed
			failedFiles.add(file);
		}

		return false;
	}

}
